﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using NCrontab;
using System.Threading;

namespace CCM.CronTaskScheduler {
    public interface IScheduleManager {
        #region Operations (15)

        TaskDefinition AddTaskDefinition(TaskDefinition taskDefinition);

        TaskDefinition AddTaskDefinition(string cronTabExpression, TaskDefinition taskDefinition);

        void ClearAllTaskDefinitions();

        DateTime GetNextScheduledTime(DateTime startTime, string cronTabExpression);

        DateTime? GetNextScheduledTime(DateTime startTime, TaskDefinition taskDefinition);

        int GetRunCount();

        int GetRunCount(string taskName);

        bool HasPendingTasks();

        bool IsBusy();

        void RunNow(string taskName);

        void RunNow(TaskDefinition taskDefinition);

        void Start();

        void Stop(bool forceClose = false);

        bool TaskExists<T>() where T : StandardTask;

        bool TaskExists(string taskName);

        #endregion Operations
    }

    public class ScheduleManager : IScheduleManager {
        #region Fields (4)

        private INotificationProvider notificationProvider;
        private IRunTimeManager runTimeManager;
        private Timer scheduleTimer;
        private ITaskRepository taskRespository;

        #endregion Fields

        #region Constructors (1)

        public ScheduleManager(ITaskRepository taskRepository, IRunTimeManager runTimeManager, INotificationProvider notificationProvider) {
            this.notificationProvider = notificationProvider;
            this.taskRespository = taskRepository;
            this.runTimeManager = runTimeManager;
            notificationProvider.AddRunCompleteHandler(RunComplete);
        }

        #endregion Constructors

        #region Methods (21)

        // Public Methods (15) 

        public TaskDefinition AddTaskDefinition(TaskDefinition taskDefinition) {
            Require("TaskName", taskDefinition.TaskName);
            Require("TaskType", taskDefinition.TaskType);

            taskDefinition.UniqueID = taskDefinition.UniqueID ?? taskDefinition.TaskName;

            if (taskRespository.AllTaskDefinitions.Any(a => a.UniqueID == taskDefinition.UniqueID))
                throw new InvalidOperationException("Task already defined with UniqueID of " + taskDefinition.UniqueID);

            if (taskDefinition.TaskType.IsSubclassOf(typeof(StandardTask)) && taskDefinition.ExecutorType == null) {
                taskDefinition.ExecutorType = typeof(StandardTaskExecutor);
            } else if (taskDefinition.ExecutorType == null) {
                Require("ExecutorType", taskDefinition.ExecutorType);
            }
            taskRespository.AllTaskDefinitions.Add(taskDefinition);
            taskRespository.RunStats.Add(taskDefinition, new RunStat());

            return taskDefinition;
        }

        public TaskDefinition AddTaskDefinition(string cronTabExpression, TaskDefinition taskDefinition) {
            taskDefinition = AddTaskDefinition(taskDefinition);
            AddSchedule(cronTabExpression, taskDefinition);
            return taskDefinition;
        }

        public void ClearAllTaskDefinitions() {
            taskRespository.AllTaskDefinitions.Clear();
        }

        public DateTime GetNextScheduledTime(DateTime startTime, string cronTabExpression) {
            startTime = startTime.ToTheMinute();
            var endTime = startTime.AddSeconds(59);
            var schedule = CrontabSchedule.Parse(cronTabExpression);
            var nextRunTime = schedule.GetNextOccurrence(startTime, endTime);
            nextRunTime = new DateTime(nextRunTime.Year, nextRunTime.Month, nextRunTime.Day, nextRunTime.Hour, nextRunTime.Minute, 0);
            return nextRunTime;
        }

        public DateTime? GetNextScheduledTime(DateTime startTime, TaskDefinition taskDefinition) {
            if (!taskRespository.Schedules.ContainsKey(taskDefinition)) return null;

            var schedules = taskRespository.Schedules[taskDefinition];

            return schedules.Select(s => GetNextScheduledTime(startTime, s)).Min();
        }

        public int GetRunCount() {
            return taskRespository.RunStats.Sum(rs => rs.Value.Error + rs.Value.Success);
        }

        public int GetRunCount(string taskName) {
            var stat = GetRunStat(GetDef(taskName));
            return stat.Error + stat.Success;
        }

        public bool HasPendingTasks() {
            return runTimeManager.PendingTasks.Count > 0;
        }

        public bool IsBusy() {
            return runTimeManager.ExecutingTasks.Count > 0;
        }

        public void RunNow(string taskName) {
            var task = GetDef(taskName);
            GetRunStat(task).LastRunTime = DateTime.Now;
            runTimeManager.QueueTask(task);
        }

        public void RunNow(TaskDefinition taskDefinition) {
            runTimeManager.QueueTask(taskDefinition);
        }

        public void Start() {
            scheduleTimer = new Timer(QueueScheduledTasks, this, TimeSpan.FromSeconds(60 - DateTime.Now.Second), TimeSpan.FromMinutes(1));
        }

        private static void QueueScheduledTasks(object state) {
            var sm = state as ScheduleManager;
            sm.taskRespository.Schedules
                .Where(s => sm.IsReadyToRunNow(s))
                .ToList()
                .ForEach(f => sm.RunNow(f.Key));
        }

        private bool IsReadyToRunNow(KeyValuePair<TaskDefinition, List<string>> scheduledTask) {
            var now = DateTime.Now.ToTheMinute();
            var result = GetNextScheduledTime(now, scheduledTask.Key);
            return result == now;
        }

        public void Stop(bool forceClose = false) {
            if (!forceClose && IsBusy()) {
                while (IsBusy())
                    Thread.Sleep(100);
            } else {
                runTimeManager.EndAllTasks();
            }

            scheduleTimer.Dispose();
            scheduleTimer = null;
        }

        public bool TaskExists<T>() where T : StandardTask {
            return taskRespository.AllTaskDefinitions.Any(a => a.TaskType == typeof(T));
        }

        public bool TaskExists(string taskName) {
            return taskRespository.AllTaskDefinitions.Any(a => a.TaskName == taskName);
        }
        // Private Methods (6) 

        private void AddSchedule(string cronTabExpression, TaskDefinition taskDefinition) {
            if (!taskRespository.Schedules.ContainsKey(taskDefinition))
                taskRespository.Schedules.Add(taskDefinition, new List<string>());
            taskRespository.Schedules[taskDefinition].Add(cronTabExpression);
        }

        private TaskDefinition GetDef(string taskNameOrUniqueId) {
            var result = taskRespository.AllTaskDefinitions
                .FirstOrDefault(f => f.UniqueID == taskNameOrUniqueId);
            if (result != null) return result;

            var results = taskRespository.AllTaskDefinitions
                .Where(f => f.TaskName == taskNameOrUniqueId);

            if (results.Count() == 1) return results.First();

            throw new InvalidOperationException("Task not found with UniqueID or name of " + taskNameOrUniqueId);
        }

        private TaskDefinition GetDef(Type taskType) {
            return taskRespository.AllTaskDefinitions.FirstOrDefault(f => taskType == f.TaskType);
        }

        private RunStat GetRunStat(TaskDefinition taskDefinition) {
            if (!taskRespository.RunStats.ContainsKey(taskDefinition))
                taskRespository.RunStats.Add(taskDefinition, new RunStat());
            return taskRespository.RunStats[taskDefinition];
        }

        private void Require(string propertyName, object value) {
            if (value == null)
                throw new Exception("Property cannot be null: " + propertyName);
        }

        private void RunComplete(TaskRunResult result) {
            var stat = GetRunStat(result.TaskDefinition);
            stat.LastRunTime = DateTime.Now;
            if (result.Success) {
                stat.Success++;
            } else {
                stat.Error++;
            }
        }

        #endregion Methods


    }
}
